﻿/* Copyright (C) 2012 by John Cronin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Net.Sockets;
using System.Net;
using System.Xml;
using System.IO;

namespace davserv
{
    partial class HttpServer
    {
        int port = 5250;
        List<TcpListener> listeners = new List<TcpListener>();
        TcpListener listener = null;
        bool cont = true;

        Random random = new Random();

        const int DefaultPort = 80;
        const string HttpVersion = "HTTP/1.1";
        const string ServerDirectory = "c:/varwww";
        const string ServerFile = "c:/www.sql";
        const string ErrorDirectory = "c:/varwwwerror";
        const string ServerVersionString = "davserv/0.1";

        public class HttpMethodHandler
        {
            public string Name;
            public davserv.Acl.PermissionType PermissionType;
            public delegate Response HandlerType(Request r, uri uri, BackendManager bm, BackendManager.BackendManagerEntry b);
            public HandlerType Handler;
        }

        BackendManager bm = new BackendManager();

        static Dictionary<string, ErrorCode> error_html = new Dictionary<string, ErrorCode>();
        Dictionary<string, HttpMethodHandler> handlers = new Dictionary<string, HttpMethodHandler>();

        class ErrorCode { public string Code; public string Reason; public string HTML; }

        public HttpServer()
        {
            ParseOptions("davserv.conf");

            IPAddress addr = IPAddress.Parse("192.168.0.16");
            listener = new TcpListener(addr, port);

            //bm.AddBackend(uri.Parse("/"), new FSBackend.FSBackend(ServerDirectory));
            //bm.AddBackend(uri.Parse("/"), new backends.sqlite.sqlite(ServerFile));

            InitErrorHTML("200", "OK");
            InitErrorHTML("207", "Multi-Status");
            InitErrorHTML("400", "Bad Request");
            InitErrorHTML("401", "Unauthorized");
            InitErrorHTML("402", "Payment Required");
            InitErrorHTML("403", "Forbidden");
            InitErrorHTML("404", "Not Found");
            InitErrorHTML("405", "Method Not Allowed");
            InitErrorHTML("406", "Not Acceptable");
            InitErrorHTML("407", "Proxy Authentication Required");
            InitErrorHTML("408", "Request Time-Out");
            InitErrorHTML("409", "Conflict");
            InitErrorHTML("410", "Gone");
            InitErrorHTML("411", "Length Required");
            InitErrorHTML("412", "Precondition Failed");
            InitErrorHTML("413", "Request Entity Too Large");
            InitErrorHTML("414", "Request-URI Too Large");
            InitErrorHTML("415", "Unsupported Media Type");
            InitErrorHTML("416", "Requested range not satisfiable");
            InitErrorHTML("417", "Expectation Failed");
            InitErrorHTML("500", "Internal Server Error");
            InitErrorHTML("501", "Not Implemented");
            InitErrorHTML("502", "Bad Gateway");
            InitErrorHTML("503", "Service Unavailable");
            InitErrorHTML("504", "Gateway Time-out");
            InitErrorHTML("505", "HTTP Version not supported");

            System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(ErrorDirectory);
            if (di.Exists)
            {
                foreach (System.IO.FileInfo fi in di.GetFiles())
                {
                    if (fi.Extension == "html")
                    {
                        string code = fi.Name.Substring(0, fi.Name.Length - 5);
                        if (error_html.ContainsKey(code))
                            error_html[code].HTML = ReadFile(fi.FullName);
                    }
                }
            }

            handlers["GET"] = new HttpMethodHandler { Handler = GET, Name = "GET", PermissionType = Acl.PermissionType.Read };
            handlers["DELETE"] = new HttpMethodHandler { Handler = DELETE, Name = "DELETE", PermissionType = Acl.PermissionType.Write };
            handlers["MKCOL"] = new HttpMethodHandler { Handler = MKCOL, Name = "MKCOL", PermissionType = Acl.PermissionType.Write };
            handlers["OPTIONS"] = new HttpMethodHandler { Handler = OPTIONS, Name = "OPTION", PermissionType = Acl.PermissionType.Read };
            handlers["PROPFIND"] = new HttpMethodHandler { Handler = PROPFIND, Name = "PROPFIND", PermissionType = Acl.PermissionType.Read };
            handlers["PUT"] = new HttpMethodHandler { Handler = PUT, Name = "PUT", PermissionType = Acl.PermissionType.Write };
        }

        private void ParseOptions(string filename)
        {
            XmlDocument d = new XmlDocument();
            d.Load(filename);

            foreach (XmlNode n in d.SelectNodes("/davserv/listen"))
            {
                string s = n.InnerText;
                string[] split = s.Split(':');
                string host = split[0];
                int port = HttpServer.DefaultPort;
                if (split.Length > 0)
                    port = int.Parse(split[1]);
                listeners.Add(new TcpListener(IPAddress.Parse(host), port));
            }

            foreach (XmlNode n in d.SelectNodes("/davserv/location"))
            {
                uri uri = uri.Parse(n.SelectSingleNode("@uri").Value);

                foreach (XmlNode b in n.ChildNodes)
                {
                    if (b.Name == "backend")
                    {
                        string btype = b.SelectSingleNode("@type").Value;

                        if (btype == "sqlite")
                        {
                            string fname = b.SelectSingleNode("sqlitefile").InnerText;
                            bm.AddBackend(uri, new backends.sqlite.sqlite(fname));
                        }
                        else
                            throw new Exception("Unknown backend: " + btype);
                    }
                    else
                    {
                        string name = b.Name;
                        string value = b.InnerText;

                        if (name.StartsWith("no_"))
                        {
                            name = name.Substring(3);
                            value = null;
                        }

                        bm.AddConfigurationObject(name, uri, value);
                    }
                }
            }
        }

        private string ReadFile(string path)
        {
            System.IO.FileStream fs = new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read);
            byte[] buf = new byte[fs.Length];
            fs.Read(buf, 0, buf.Length);
            fs.Close();
            return System.Text.Encoding.ASCII.GetString(buf);
        }

        void InitErrorHTML(string code, string name)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<HTML><HEAD><TITLE>");
            sb.Append(name);
            sb.Append("</TITLE><BODY><H1>");
            sb.Append(code);
            sb.Append(": ");
            sb.Append(name);
            sb.Append("</H1></BODY></HTML>");

            error_html.Add(code, new ErrorCode { Code = code, Reason = name, HTML = sb.ToString() });
        }

        public void Start()
        {
            listener.Start();

            while (cont)
            {
                TcpClient client = listener.AcceptTcpClient();
                System.Diagnostics.Debug.WriteLine("Accepted client connection from: " + client.Client.RemoteEndPoint.ToString());
                HandleClient(client);
            }
        }

        void HandleClient(TcpClient client)
        {
            NetworkStream stream = client.GetStream();
            Response response;

            try
            {
                Request r = ReadMessage(stream);
                r.Client = client;
                try
                {
                    r.ClientHostnameWait = System.Net.Dns.BeginGetHostEntry(((System.Net.IPEndPoint)client.Client.RemoteEndPoint).Address, null, null);
                }
                catch (Exception)
                {
                    r.ClientHostname = "unknown";
                }

                response = HandleRequest(r);
            }
            catch (NotImplementedException e)
            {
                response = Response.MakeErrorResponse("501");
                System.Diagnostics.Debug.WriteLine(e.Message);
            }
            catch (Exception e)
            {
                response = Response.MakeErrorResponse("500");
                System.Diagnostics.Debug.WriteLine(e.Message);
            }
            
            SendMessage(stream, response);
            stream.Close();
        }

        void SendMessage(NetworkStream stream, Response r)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(r.HTTPVersion);
            sb.Append(" ");
            sb.Append(r.StatusCode);
            sb.Append(" ");
            sb.Append(r.ReasonPhrase);
            sb.Append("\r\n");

            if (r.Headers.Count > 0)
            {
                foreach (KeyValuePair<string, string> kvp in r.Headers)
                {
                    sb.Append(kvp.Key);
                    sb.Append(": ");
                    sb.Append(kvp.Value);
                    sb.Append("\r\n");
                }

                sb.Append("\r\n");
            }
            else
                sb.Append("\r\n\r\n");

            List<byte> msg = new List<byte>(System.Text.Encoding.ASCII.GetBytes(sb.ToString()));
            if(r.Body != null)
                msg.AddRange(r.Body);

            stream.Write(msg.ToArray(), 0, msg.Count);
        }

        void SendMessage(NetworkStream stream, string response)
        {
            byte[] resp = System.Text.Encoding.ASCII.GetBytes(response);
            stream.Write(resp, 0, resp.Length);
        }

        Request ReadMessage(NetworkStream s)
        {
            byte[] buf = new byte[1];
            List<byte> blist = new List<byte>();
            bool read = true;

            int double_enter_read = 0;

            while (s.DataAvailable && read)
            {
                s.Read(buf, 0, buf.Length);

                blist.Add(buf[0]);

                if (((double_enter_read == 0) || (double_enter_read == 2)) &&
                    (buf[0] == '\r'))
                    double_enter_read++;
                else if (((double_enter_read == 1) || (double_enter_read == 3)) &&
                    (buf[0] == '\n'))
                    double_enter_read++;
                else
                    double_enter_read = 0;

                if (double_enter_read == 4)
                    read = false;
            }
            
            // we have read the headers, now parse them
            string headers = System.Text.Encoding.ASCII.GetString(blist.ToArray());
            Request ret = ParseHeaders(headers);

            if (ret.Headers.ContainsKey("Content-Length"))
            {
                ret.Body = new byte[Int32.Parse(ret.Headers["Content-Length"])];
                s.Read(ret.Body, 0, ret.Body.Length);
            }

            return ret;
        }

        Request ParseHeaders(string headers)
        {
            try
            {
                Request r = new Request();

                string[] lines = headers.Split(new string[] { "\r\n" }, StringSplitOptions.None);
                string[] line0 = lines[0].Split(new char[] { ' ' }, StringSplitOptions.None);

                r.Method = line0[0];
                r.RequestURI = line0[1];
                r.HTTPVersion = line0[2];

                for (int i = 1; i < lines.Length; i++)
                {
                    if (lines[i] == "")
                        break;

                    int colon = lines[i].IndexOf(": ");

                    string param = lines[i].Substring(0, colon);
                    string val = lines[i].Substring(colon + 2);

                    r.Headers.Add(param, val);
                }

                return r;
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine("Error parsing request headers: " + headers + "\n" + e.ToString());
                throw new Exception("Parse exception");
            }
        }

        Response HandleRequest(Request r)
        {
            davserv.uri uri = davserv.uri.Parse(r.RequestURI);
            BackendManager.BackendManagerEntry b = bm.GetBackend(uri);

            if (handlers.ContainsKey(r.Method))
            {
                HttpMethodHandler handler = handlers[r.Method];

                davserv.Acl.Permission permission = davserv.Acl.CheckPermissions(uri, bm, r, handler.PermissionType);

                if (permission.Allowed)
                    return handler.Handler(r, uri, bm, b);
                else
                {
                    if (permission.AuthRequested)
                    {
                        string auth_realm = bm.GetConfigurationObject("auth_realm", uri).ConfigurationObject;

                        if (bm.GetConfigurationObject("auth_method", uri).ConfigurationObject == "digest")
                        {

                            Response resp = Response.MakeErrorResponse("401");

                            long ticks = System.DateTime.Now.Ticks;
                            byte[] rnd = new byte[8];
                            random.NextBytes(rnd);
                            long rand = System.BitConverter.ToInt64(rnd, 0);
                            long l_nonce = ticks ^ rand;
                            string nonce = System.Convert.ToBase64String(System.BitConverter.GetBytes(l_nonce));

                            string opaque = System.Convert.ToBase64String(System.BitConverter.GetBytes(~l_nonce));

                            string auth_header = "Digest realm=\"" + auth_realm + "\", qop=\"auth\", nonce=\"" + nonce + "\", opaque=\"" + opaque + "\"";

                            resp.Headers.Add("WWW-Authenticate", auth_header);

                            return resp;
                        }
                    }
                    return Response.MakeErrorResponse("403");
                }
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("Unknown request: " + r.Method);
                return Response.MakeErrorResponse("400");
            }
        }

        internal static string MakeContentTypeHeader(Backend.ByteResponse resp)
        {
            string ret = resp.ContentType;
            if (resp.Encoding != null)
                ret = ret + "; charset=" + resp.Encoding;
            return ret;
        }

        class PropertyValue
        {
            public Backend.PropertyName Name;
            public string Status;
            public string Response;
        }

        internal class Request
        {
            public string Method;
            public string RequestURI;
            public string HTTPVersion;
            public string StringBody { get { if (Body == null) return null; return System.Text.Encoding.ASCII.GetString(Body); } }
            public TcpClient Client;
            public string ClientHostname;
            internal IAsyncResult ClientHostnameWait;

            public byte[] Body;
            public Dictionary<string, string> Headers = new Dictionary<string, string>();

            public System.Xml.XmlNode XmlBody
            {
                get
                {
                    if (!Headers.ContainsKey("Content-Type"))
                        return null;

                    if ((Headers["Content-Type"] == "application/xml") ||
                        (Headers["Content-Type"] == "text/xml"))
                    {
                        System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
                        doc.LoadXml(StringBody);
                        return doc.DocumentElement;
                    }

                    return null;
                }
            }
        }

        public class Response
        {
            public string HTTPVersion;
            public string StatusCode;
            public string ReasonPhrase;
            public Dictionary<string, string> Headers = new Dictionary<string, string>();
            public string StringBody
            {
                get { if (Body == null) return null; return System.Text.Encoding.ASCII.GetString(Body); }
                set { Body = System.Text.Encoding.ASCII.GetBytes(value); }
            }
            public byte[] Body;

            public Response()
            {
                Headers.Add("Date", DateTime.Now.ToString("r"));
                Headers.Add("Server", HttpServer.ServerVersionString);
            }

            public static Response MakeSuccessResponse(string body)
            {
                Response r = new Response();
                r.HTTPVersion = HttpServer.HttpVersion;
                r.StatusCode = "200";
                r.ReasonPhrase = "OK";
                r.StringBody = body;

                r.Headers["Content-Length"] = System.Text.Encoding.ASCII.GetByteCount(body).ToString();

                return r;
            }

            public static Response MakeResponse(string code, byte[] val)
            {
                Response r = new Response();
                r.HTTPVersion = HttpServer.HttpVersion;
                r.StatusCode = code;
                r.ReasonPhrase = HttpServer.error_html[code].Reason;
                r.Body = val;

                r.Headers["Content-Length"] = val.Length.ToString();

                return r;
            }

            public static Response MakeErrorResponse(string code)
            {
                Response r = new Response();
                r.HTTPVersion = HttpServer.HttpVersion;
                r.StatusCode = code;
                r.ReasonPhrase = HttpServer.error_html[code].Reason;
                r.StringBody = HttpServer.error_html[code].HTML;

                r.Headers["Content-Length"] = r.Body.Length.ToString();

                return r;
            }
        }
    }
}
